Skip to content

feat(themes)!: replace eight presets with three deliberate ones - #11

Open
aloks98 wants to merge 6 commits into
mainfrom
feat/three-themes
Open

feat(themes)!: replace eight presets with three deliberate ones#11
aloks98 wants to merge 6 commits into
mainfrom
feat/three-themes

Conversation

@aloks98

@aloks98 aloks98 commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Breaking: replaces the eight theme presets with three.

amber, crimson, forest, ocean, oxide, rose, slate and violet are removed, along with their subpath exports. They were one neutral shell with the accent hue swapped — over-saturated, muddy in dark mode, and named for color rather than for use. Eight mediocre options are worse than three deliberate ones.

New slug Character Body / Headings Radius
press Editorial — warm paper, near-black ink, high contrast Source Sans 3 / Source Serif 4 0.5rem
console Technical — dense, low-chroma, terminal-adjacent Geist / Geist Mono 0.125rem
signal Brand-forward — warm neutral shell, one confident accent Instrument Sans / Space Grotesk 0.75rem

Migration

Every [data-theme="…"] value changes. Map by the intent behind the old choice, not by hue:

Was Use Why
slate, oxide press Editorial/professional, serif-or-substantial headings
ocean, violet console Cool, technical, restrained
rose, amber, crimson signal Warm shell carried by one confident accent
forest press or signal Depending on whether the serif pairing mattered
- <html data-theme="ocean">
+ <html data-theme="console">
- @import '@e412/rnui-themes/ocean';
+ @import '@e412/rnui-themes/console';

If you were extending a preset, re-point the selector — the token names are unchanged, so overrides carry over as-is:

- [data-theme='ocean'] { --primary: oklch(0.48 0.15 220); }
+ [data-theme='console'] { --primary: oklch(0.48 0.15 220); }

What else changed

  • Every theme now defines --font-heading and --font-mono. Previously only two of the eight set a heading face; the rest silently fell back to the system stack, which made them look unfinished next to the ones that didn't.
  • Complete dark ramps. Dark surfaces step monotonically by at least 4% lightness, so elevation reads as elevation instead of noise, and dark chroma never exceeds the light value for the same token — raising chroma to compensate for darkness is what produced the muddiness.
  • Accessibility is a floor, not a preset. Every */*-foreground pair clears WCAG AA in both modes (4.5:1 text, 3:1 borders and rings), verified in the build by scripts/check-contrast.mjs, which fails on a regression. There is no separate "high contrast" theme: press is the high-contrast option by character, and the floor applies to all three equally.
  • The default theme (no data-theme) also now meets that floor. Its look is otherwise unchanged, but 17 tokens moved to clear AA — text-warning was 1.89:1 and text-success 2.45:1 against the page background.
  • One accent per theme. --primary, --focus, --ring and --sidebar-primary derive from a single hue; --accent is a tint of it rather than a second color. Status colors (--success, --info, --warning, --destructive) keep the same hues across all three so they stay recognizable.
  • Theme sources are generated from an anchor spec by scripts/generate-themes.mjs; edit the anchors there rather than the emitted CSS.

Verification

  • check-contrast.mjs runs in the build: 4 themes pass AA in both modes (4.5:1 text, 3:1 borders/rings). It fails the build on regression.
  • Baseline before this change: 294 failures across the old presets — including text-warning at 1.89:1 and dark surfaces separated by 0.0% lightness.
  • Full pnpm build green; format:check clean; lint 0 errors.
  • Design-sync re-run: driver ok: true, nothing pending, 73 components carried forward, 0 grades cleared.
  • Fonts subset 0.55 MB → 0.33 MB (10 families → 6).

Reviewing this

The ramps are generated, so review scripts/generate-themes.mjs (the anchors and derivation) and scripts/check-contrast.mjs (the rules) rather than the ~270 emitted values in src/*.css.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NAmWWJ2twyebZgPLMc1cg1

aloks98 and others added 6 commits August 6, 2026 18:04
Syncs the design system to claude.ai/design so its design agent builds
with the real compiled @e412/rnui-react components. All 72 components
verified against this repo's own Storybook render.

- config.json: converter config (titleMap, docsMap, cardMode overrides,
  extraEntries, story skips). Paths are PKG_DIR-relative except
  readmeHeader, which resolves from the config home.
- conventions.md: usage guide prepended to the generated README. Its key
  point is that Tailwind v4 has no runtime compiler in Claude Design, so
  only pre-compiled utilities resolve; it enumerates the real vocabulary.
- previews/Slider.tsx: owned preview restoring Slider's card (its stories
  are unrenderable in Storybook — see NOTES.md).
- NOTES.md: config rationale, triaged warnings, and re-sync risks.

Also documents a repo bug: 5 stories in slider/progress use a percentage
width under the global layout:'centered', so they collapse to 0px and are
invisible in Storybook itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NAmWWJ2twyebZgPLMc1cg1
Claude Design reported "Missing brand fonts ... rendering with
substitutes": the 8 presets name 10 web fonts, but @e412/rnui-themes
deliberately doesn't load them, so every preset fell back to a system
stack in rendered designs.

Self-hosts them instead: fetch-fonts.mjs pulls the variable woff2 from
Google Fonts (all 10 families are SIL OFL 1.1) into .design-sync/fonts/,
and cfg.extraFonts makes the build copy them into ds-bundle/fonts/ and
@import the sheet from styles.css — the closure designs receive.
20 faces (latin + latin-ext), 0.55 MB.

Verified in chromium: 20 faces registered, 0 failed requests, all 10
families pass document.fonts.check(), and every preset resolves to its
real body face (oxide/DM Sans ... crimson/Instrument Sans).

extraFonts is outside the grade key, so no component grades were
invalidated — the driver receipt stayed 72 carried forward, 0 cleared.

conventions.md previously told the design agent these fonts were NOT
bundled; corrected, with a per-theme typography table.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NAmWWJ2twyebZgPLMc1cg1
Both story files size their root with a percentage width (`w-[60%]`)
while .storybook/preview.tsx applies a global `layout: 'centered'`.
A centered story root is shrink-to-fit, so the percentage resolved
against a 0px parent: the components mounted with full Base UI DOM but
measured 0px wide and rendered nothing. Base UI computed
`--start-position: NaN%` / `Infinity%` for the slider indicator.

Five stories were affected and invisible in Storybook:
slider (Default, Range) and progress (Default, Empty, Full).

Adds `parameters: { layout: 'padded' }` to both metas — the same thing
date-selector.stories.tsx already does, which is why its `w-full
max-w-xl` stories always rendered. A padded root spans the canvas, so
the percentage resolves normally.

Verified: all 6 stories across the two components now capture with 0
factual failures (previously 5 were sb-error) and grade match against
their previews.

Removes the sync workarounds these stories forced:
- overrides.{Slider,Progress}.skip — both sets capture now
- .design-sync/previews/Slider.tsx — the owned stopgap that stood in
  for Slider's unphotographable card
- Progress keeps cardMode "single" (its grid-cell overflow is a card
  layout concern, unrelated to the story bug)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NAmWWJ2twyebZgPLMc1cg1
rnui is normally consumed with Tailwind in the consumer's build, so any
class compiles on demand. But it is also consumed precompiled — as one
stylesheet embedded in a design tool — and there a class Tailwind never
emitted silently does nothing. Ordinary layout markup (gap-10,
grid-cols-4, max-w-4xl, text-3xl) hit this constantly.

Adds:

- src/safelist.css — complete scales, not just the values our own
  components use: spacing 0-24 on every box-model utility, text-xs..9xl,
  grid-cols-1..12, col-span-*, max-w-xs..7xl, radius/border/opacity, plus
  sm:/md:/lg:/xl: and hover:/focus:/active:/disabled:/dark: on the
  interactive subset.
- src/utilities.css -> dist/utilities.css (~353 KB, 40 KB gzip), the
  precompiled layer for consumers who cannot run Tailwind.
- dist/utilities.json — 4964 classes, derived from the COMPILED output
  rather than the safelist source, so tooling can report what actually
  shipped.
- dist/tokens.json — 399 tokens across 9 themes with name/theme/kind/
  value/light/dark. Built from hand-authored theme sources, never from
  compiled CSS, so Tailwind internals are excluded by construction. The
  build fails if any leak in. Tokens omitted from a dark ramp are flagged
  darkInherited instead of being presented as declared.

apps/storybook imports the safelist because the design-sync bundle is
scraped from that build; the safelist has to be in it, not only in the
themes package.

The safelist lives in themes rather than react because react already
depends on themes — the reverse would be a workspace cycle. Themes
therefore safelists only what it can compile alone; component classes
still come from the Storybook scrape.

Also prefixes the private custom properties --cell-size/--cell-radius
(Calendar) and --data-grid-fill-size (DataGrid) with --rnui-_ so they are
unambiguously not design tokens. --sidebar-width is left alone: it is set
via a style prop on SidebarProvider, a supported consumer override.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NAmWWJ2twyebZgPLMc1cg1
The new exports had no documentation, and one existing doc was actively
wrong after the change.

- packages/themes/README.md: adds "Precompiled consumers" (when to use
  utilities vs. safelist vs. neither) and a machine-readable token export
  section, plus the two new entries in the Imports list.
- apps/docs theming.mdx: adds "Reading Tokens Programmatically" — read
  tokens.json rather than scanning compiled CSS, since a CSS scan reports
  Tailwind plumbing as part of the palette — and "Using rnui Without a
  Tailwind Build".
- README.md: the themes package summary now mentions both.

Also corrects .design-sync/conventions.md, which is inlined into the
design agent's prompt. It enumerated the OLD narrow vocabulary (gap
stopped at 6, no grid-cols past 3, no responsive or state variants), so
after the safelist landed it would have steered the agent away from
classes that now work. Rewritten against the shipped bundle; every class
named in it is verified present.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NAmWWJ2twyebZgPLMc1cg1
BREAKING CHANGE: amber, crimson, forest, ocean, oxide, rose, slate and
violet are removed along with their subpath exports. They were one
neutral shell with the accent hue swapped — over-saturated, muddy in
dark mode, and named for colour rather than for use.

Ships three instead, named for what they are for:

  press    Editorial — warm paper, near-black ink, high contrast
           Source Sans 3 / Source Serif 4, radius 0.5rem
  console  Technical — dense, low-chroma, terminal-adjacent
           Geist / Geist Mono, radius 0.125rem
  signal   Brand-forward — warm neutral shell, one confident accent
           Instrument Sans / Space Grotesk, radius 0.75rem

Migration is in the changeset: slate/oxide -> press, ocean/violet ->
console, rose/amber/crimson -> signal. Token names are unchanged, so
overrides carry over by re-pointing the selector.

Accessibility is now a floor rather than a preset. check-contrast.mjs
runs in the build and fails it on regression: 4.5:1 for text pairs, 3:1
for borders and rings, in both modes. It encodes the real pair semantics
taken from component usage — primary/primary-foreground is text-on-solid,
while status colours are text on surfaces and on their own 10% tint — and
composites alpha before measuring, since an alpha'd border measured as
opaque reports ~20:1 for a line that renders faint.

The default theme is held to that same floor: 17 tokens moved to clear
AA (text-warning was 1.89:1, text-success 2.45:1 against the page). Its
look is otherwise untouched — the ramp-structure rules apply only to the
generated presets, since restyling the out-of-the-box theme was not part
of this change.

Ramps are derived by generate-themes.mjs from a small anchor spec rather
than hand-tuned, so the numeric constraints hold by construction: dark
surfaces step >= 4% lightness monotonically, dark chroma never exceeds
its light counterpart, neutrals are never literally gray, and every
theme declares --font-sans, --font-heading and --font-mono with complete
light and dark ramps.

Fonts are subset to the six families the three themes reference:
0.55 MB -> 0.33 MB, dropping Bitter, DM Sans, Fraunces, Nunito, Outfit,
Plus Jakarta Sans and Sora.

Docs, design-sync conventions and the synced project are updated; stale
preset and font files were reconciled out of the design project, which
an anchor-based diff cannot see on its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NAmWWJ2twyebZgPLMc1cg1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant